Disqueメモ
概要
訳した。
https://github.com/antirez/disque
とりあえず使ってみて俯瞰で訳したみたいな感じなんだけど、ひたすらGoogle翻訳と相性の悪い文章だった、、、
間違ってそうなところあったら優しく教えて欲しい。
あ、プルリクしてくれると楽。rtf or rtfdが日付フォルダに入ってるのでそれを更新してpull-reqくれれば嬉しい。
https://github.com/sassembla/Public/tree/gh-pages
Give me the details!
DISQUEは、プロセスの間でメッセージを交換する中間層として機能する、分散型のフォールトトレラントなメッセージブローカーです。
producerがconsumerに提供されるメッセージを追加します。
メッセージキューはしばしば遅延ジョブを処理するために使用されるので、DISQUEはjobという単位をAPIとかドキュメントの中で使っています。
とはいえ、実際のjobとは、実際には文字列の形で扱われる単なるメッセージなので、いろんなケースで使えると思っています。
この文書内では、「job」と「message」が示すものはイコールな感じです。
producer-consumer モデルとジョブ・キューはかなり似ているので、悪魔は細部に宿るような感じになりました。
DISQUEについての詳細は、次のとおりです。
DISQUEは同期的に複製されるジョブ・キューです。
キューに新しいjobが追加された際、クライアント(producerかconsumer)がそのjobについて確認できる状態になる前に、メッセージはW個のノードに複製されます。
W-1個のノードに障害が発生しても、メッセージを配信することができます。
DISQUEは「少なくとも一回」と「最大1回」の両方の配信セマンティクスをサポートしています。
「少なくとも一回配信する」、という部分にメッチャ力を入れました。
「最大一回」はretry time を0にセットした際の結果実現できる(決して再びメッセージを再キューイングしない) + replication 要素を1にセット(厳密にはこの設定必要ではないんだけど、メッセージの複数のコピーを持つのが無駄なので)という設定で実現できるようになっています。
これらはメッセージごとに設定が可能なので、同じキューやノードに対して「少なくとも一回」と「最大一回」という設定を同時に持つことができます。
DISQUEは障害のただなかにあっても、「少なくとも一回」配信が可能なように設計されています。
これは、「DISQUEは最低でも一つか、それ以上の回数の配信を保証することができるが、可能な限り過剰な配信を回避する」、ということを意味します。
DISQUEはすべてのノードが同じ役割を持つ分散型のシステム(マルチマスターとか呼ばれる感じのやつ)です。
producerとconsumerは、彼らが好きなノードに接続することができるし、同じキューに対してのproducer, consumerである必要がありません。自動的に同じノードへの接続を維持します。ノードはロードとクライアントの要求に基づいて、メッセージを交換します。
DISQUEはCAP定理でいえばeventuallyに一貫性のあるAPとして利用可能です。
少なくとも一つのノードが到達可能であるうちは、producerとconsumerは処理を進めることができます。
DISQUEは非同期コマンドをサポートしていて、これはクライアントにとって低遅延ですが、保証の内容が低下します。
例えばproducerがreplication = 3をセットした設定のキューにジョブを追加した際、そのメッセージを追加されたノードが本当にreplication = 3の設定を果たせたかどうかを知る前に処理を返したい感じです。その際、ノードはベストエフォート型の方法で、バックグラウンドでメッセージを複製します。
DISQUEはとあるメッセージに対して、指定されたretry time後にconsumerがメッセージを処理することができなかった(その通知がなかった)場合、自動的にそのメッセージを再キューイングします。
consumer側がメッセージを明示的に再キューイングする処理は必要ない感じです。
DISQUEは、consumerが確実にメッセージを処理した(別の言い方をすれば、すでに処理されたジョブを通知する)ということを知るために、明示的な通知手段を用います。
DISQUEのキューは、ぶっちゃけベストエフォート型の順序保証を提供します。
それぞれのキューはメッセージの順番をその生成時間から決めています。メッセージが作成されたノードのwallclockが基準になります。
同じノードで作成されたメッセージは通常、作成された順番で配信される感じです。
異なるnodeの異なるwallclockを基準にしてるので、これはまあ一般的なorderingではないです。例えば、次のようなケース。
・メッセージがretry time内で配信されず、ノードのロードバランシングと調停処理を加味して別のノードに再キューイングされた時
(この際、consumerは元のメッセージを作ったのとは別のノードから、そのノードが自分のwallclockで作成し直したメッセージを受け取ることになります。)
しかしまあ、メッセージが必ずしもランダムな順序で配信されるか、というとそうではなく、通常は最初に作成されたメッセージが最初に配信される感じです。
DISQUEは厳格なFIFOのセマンティクスを提供しません。なので、技術的に言えばメッセージキューと呼ばれるべきではありません。正しくは、いい感じのメッセージブローカー、です。
しかし、私は現時点では、IT業界でのメッセージキューは多くの場合、あんまり深刻に考えずに、別に順序を保証することが必須ではないもの、、ようはブローカーとして使用されているんじゃないかな~と思っています。
その意味で、私はDISQUEをメッセージキューと呼んでいいんじゃないか、って考えています。
DISQUEはメッセージのコントロールに際して、3つの時間関連のパラメータを提供しています。あと一個、replicationに関するパラメータがあります。
これらを使って、job(メッセージ)の以下のような要素がコントロールできます。
1. replication 複製: 何個のノードがメッセージの複製を持つか
2. delay 遅延: DISQUEがメッセージ成果物を作り、キューにメッセージを入れるまで待機する時間
3. retry リトライ: メッセージが配信されずに、もう一度キューイングされるまでにかかる時間
4. expire 有効期限: キューの中にメッセージが最大何秒残っていていいか(配信完了、失敗を問わずメッセージ作成からこの時間が経過したらメッセージを消す)
最後に。DISQUEはデフォルトオフなオンディスクでのメッセージ永続化をサポートしていますが、それは1つのデータセンターのセットアップ中の処理や再起動時の処理に便利です。
ACKs and retries
DISQUEでの「少なくとも一回配信」セマンティクスの実装は、特定のケースの障害中でも複数の無用な配信を回避するために設計されています。
複数の配達が発生しないことを保証することはできませんが、それでもまあ、最低でも一回といいつつ複数回の配信を許容可能な(または明示的な処理ができる)ケースがあると思ってます。守れるにこしたことはないんですが。
例としては、ユーザーに電子メールを送信するケースです。(ユーザーが重複した電子メールを取得するとか、まあ、それはひどすぎるってほどの出来事ではありませんが、可能な場合は回避するのが大事ですよね。)
複数の配信をできるだけ避けるために、DISQUEはクライアントACKというのを使用しています。
consumerはメッセージを正しく処理できた際、その事をDISQUEに伝えます。
その際、ACKは複数のノードに複製され、最速で破棄されます。クラスタの中の複数のノードが一つのACKの対象のjobを持っている事は滅多にない状況なので。
メモリ圧力下や、または特定の障害シナリオ下で、ACKは最終的に破棄されます。
具体的には以下のようなケースがあります:
1. jobが複数のノードに複製されたが、普通は一つのキューに一つのノードがあるだけなので、メモリ内にjobがあるという状態とキューイングしてあるという状態には違いがあります。(???)
2. 複数のノードがあるメッセージのコピーを持ち、ACKが得られずに一定の時間が経過した場合、メッセージは再キューイングされます。その際、複数のノードでできるだけ再キューイングが発生しないように努力するようになっています。(なのでACKは結果的に消される、みたいな話?)
3. ACKは、networkに断絶がない状態 + なんの失敗もせずにメッセージが処理された状態だと、最終的に最速で、メッセージのクラスタ間での複製とガベコレが行われます。
もしノードがjobのコピーを持っている状態で、特定のconsumerとjobとの間に断絶があり、それでもconsumerからの通知(ACK)がretry設定時間より先に到達した場合、そのACKはきちんと処理され、メッセージの再キューイングを避ける事ができます。
同様に、jobは最低でも一つのノードが利用できる場合、ACKを受け取る事ができます。また障害復帰時、そのACKはまだメッセージのコピーを保持している他のノードにも通知されます。
このようにACKは、複数回の同じメッセージ配信を避けるべく、何回か複製/再作成が行われる、メッセージが配信されたことの証明になっています。実際。
すでに述べたように、複製や再試行を制御するために、DISQUEジョブは、次の関連プロパティがあります。複製、遅延、再試行、有効期限についてのやつです。
retry=0なjobは、キューに「一回のみ」保持されます(このケースだとreplicationが1より大きくても無意味な感じになり、もしセットしたらエラーが出ます<訳注:本当に出るのか試したことないや。何ていうエラーがでるんだろうな>)。
で、そのメッセージは一発で配信されるか、配信失敗して未達のまま残るかします。
jobがディスクに保持されている間は、キューはそれらを保持しません。この動作は、永続化設定が何であれ、複数ノードがクラッシュから復帰した際に保証されています。
ただし、たとえばアップグレード時などにシステム管理者によってノードが手動で再起動された際には、キューが正しく保持され、この場合はストア/ロード操作がatomicなので、スタートアップ時にリロードされます。この時レースコンディションが発生する可能性はありません(jobがすでにclientに送付され、そのjobが同時にディスクに保存された場合を除く)。
Fast acknowledges
DISQUEは、FASTACKコマンドという「処理されたメッセージを確認するためのより高速なACK」をサポートしています。
到達通知についてガチで考えた場合、メッセージがノード間で交換されることから、それはとてもつらい手法になる感じです。
通常のACKコマンドの動作は以下のような感じです:
1. クライアントが1つのノードAにACKJOBを送信
2. ノードAは、コピーを持っている他のすべてのノードにSETACKメッセージを送信
3. それに対してすべてのノードはGOTACKで答える
4. ノードAは、最終的に他のすべてのノードにDELJOBを送信
注:なんらか失敗が発生した場合の実際のガベージコレクションはもっと複雑で、あとでステートマシンの説明が書いてあります。上記の進行で処理時間の99%にあたります。
メッセージが3つのノードに複製されている場合、重複した配信を避けるために真面目に頑張るならば、その到達通知には 1 +2 +2 +2のノード間の通信を必要とします。
で、FASTACKは、信頼性が低いながらとても高速で、より少ないメッセージ交換を行うように考えられています。
FASTACKは以下のような感じで動きます。
1. クライアントは1ノードにACKJOBを送信
2. 受け取ったノードは対象のjobを破棄、ノードがjobを認識していなかった場合はコピーを持っている可能性のあるすべてのノード か すべてのクラスタに対して対象jobのDELJOBを送信
ネットワークの断絶などによって、配信完了の通知がメッセージのコピーを保持しているノードに到達できない場合、そのノードは復帰時に、「すでに配信され、破棄されたはずのメッセージ」を再度配信してしまう可能性があります。これは、誰もそのメッセージが「すでに配信されたものだ」という情報を持っていないために起こり得ます。
あなたが使用しているネットワークの信頼性が非常に高い場合は、パフォーマンスに対して関心が向いているでしょうし、別に複数回配信されても困らないんじゃないでしょうか。というわけでFASTACKは有効な手段だと思っています。
Disque and disk persistence
Job IDs
そのうち気がむいたら。